package visitor;

import java.util.Vector;

import symboltable.Symbol;
import symboltable.SymbolTable;
import syntaxtree.And;
import syntaxtree.ArrayAssign;
import syntaxtree.ArrayLength;
import syntaxtree.ArrayLookup;
import syntaxtree.Assign;
import syntaxtree.Block;
import syntaxtree.BooleanType;
import syntaxtree.Call;
import syntaxtree.ClassDeclExtends;
import syntaxtree.ClassDeclSimple;
import syntaxtree.False;
import syntaxtree.Formal;
import syntaxtree.Identifier;
import syntaxtree.IdentifierExp;
import syntaxtree.IdentifierType;
import syntaxtree.If;
import syntaxtree.IntArrayType;
import syntaxtree.IntegerLiteral;
import syntaxtree.IntegerType;
import syntaxtree.LessThan;
import syntaxtree.MainClass;
import syntaxtree.MethodDecl;
import syntaxtree.Minus;
import syntaxtree.NewArray;
import syntaxtree.NewObject;
import syntaxtree.Not;
import syntaxtree.Plus;
import syntaxtree.Print;
import syntaxtree.Program;
import syntaxtree.StringArrayType;
import syntaxtree.This;
import syntaxtree.Times;
import syntaxtree.True;
import syntaxtree.Type;
import syntaxtree.VarDecl;
import syntaxtree.VoidType;
import syntaxtree.While;

public class TypeCheckingVisitor implements TypeVisitor {

	private SymbolTable tbl;
	
	// Performs type checking on a Program p with SymbolTable t
	public void perform_type_checking(Program p, SymbolTable t)
	{
		tbl = t;
		visit(p);
	}
	
	@Override
	public Type visit(Program n) {
		// TODO Auto-generated method stub
		// Calls recursively for the main class
		n.m.accept(this);
		// Calls recursively for the other classes
		for(int i = 0; i < n.cl.size(); i++) {
			n.cl.elementAt(i).accept(this);
		}
		return null;
	}

	@Override
	public Type visit(MainClass n) {
		// TODO Auto-generated method stub
		// Starts class scope
		tbl.begin_class_scope(Symbol.symbol(n.i1.s));
		Vector<Formal> main_args = new Vector<Formal>();
		main_args.add(new Formal(new StringArrayType(), n.i2));
		// Starts method scope
		tbl.begin_method_scope(new VoidType(), Symbol.symbol("main"), main_args);
		// Sees if the method statement are satisfied with the current table
		n.s.accept(this);
		// Finishes method scope
		tbl.end_method_scope();
		// Finishes class scope
		tbl.end_class_scope();
		return null;
	}

	@Override
	public Type visit(ClassDeclSimple n) {
		// TODO Auto-generated method stub
		// Starts class scope
		tbl.begin_class_scope(Symbol.symbol(n.i.s));
		// For each attr
		for(int i = 0; i < n.vl.size(); i++) {
			n.vl.elementAt(i).accept(this);
		}
		// For each method
		for(int i = 0; i < n.ml.size(); i++) {
			n.ml.elementAt(i).accept(this);
		}
		// Finishes class scope
		tbl.end_class_scope();
		return null;
	}

	@Override
	public Type visit(ClassDeclExtends n) {
		// TODO Auto-generated method stub
		// Starts class scope
		tbl.begin_class_scope(Symbol.symbol(n.i.s), Symbol.symbol(n.j.s));
		// For each attr
		for(int i = 0; i < n.vl.size(); i++) {
			n.vl.elementAt(i).accept(this);
		}
		// For each method
		for(int i = 0; i < n.ml.size(); i++) {
			n.ml.elementAt(i).accept(this);
		}
		// Finishes class scope
		tbl.end_class_scope();		
		return null;
	}

	@Override
	public Type visit(VarDecl n) {
		// TODO Auto-generated method stub
		// Variable declaration, we have to guarantee that the type specified is not undefined
		Type t = n.t;
		if (	!(t.equals(new IntegerType()) || t.equals(new BooleanType()) || 
				t.equals(new IntArrayType())/* || t.equals(new StringArrayType())*/) 
				&& !tbl.contains_class(Symbol.symbol(n.i.s))	) {
			// Variable type was not defined
			System.out.println("Variable " + n.i.s + " type " + n.t.toString() + " was not declared." );
		}
		return null;
	}

	@Override
	public Type visit(MethodDecl n) {
		// TODO Auto-generated method stub
		Vector<Formal> m_args = new Vector<Formal>();
		for(int i = 0; i < n.fl.size(); i++) {
			m_args.add(n.fl.elementAt(i));
		}
		// Starts method scope
		tbl.begin_method_scope(n.t, Symbol.symbol(n.i.s), m_args);
		// For every method argument
		for (Formal arg : m_args) {
			arg.accept(this);
		}
		// For each variable declaration
		for (int i = 0; i < n.vl.size(); i++) {
			n.vl.elementAt(i).accept(this);
		}
		// For each statement
		for (int i = 0; i < n.sl.size(); i++) {
			n.sl.elementAt(i).accept(this);
		}
		// Now for the method return type
		Type ret = n.e.accept(this);
		
		if (!ret.equals(n.t)) {
			System.out.println("Return type doesnt matches method declaration return type");
		}
		
		// Ends method scope
		tbl.end_method_scope();
		
		return null;
	}

	@Override
	public Type visit(Formal n) {
		// TODO Auto-generated method stub
		// Formal declaration, we have to guarantee that the type specified is not undefined
		Type t = n.t;
		if (	!(t.equals(new IntegerType()) || t.equals(new BooleanType()) || 
				t.equals(new IntArrayType()) || t.equals(new StringArrayType())) 
				&& !tbl.contains_class(Symbol.symbol(n.i.s))	) {
			// Variable type was not defined
			System.out.println("Variable " + n.i.s + " type " + n.t.toString() + " was not declared." );
		}
		return null;
	}

	@Override
	public Type visit(IntArrayType n) {
		// TODO Auto-generated method stub
		return n;
	}

	@Override
	public Type visit(StringArrayType n) {
		// TODO Auto-generated method stub
		return n;
	}

	@Override
	public Type visit(BooleanType n) {
		// TODO Auto-generated method stub
		return n;
	}

	@Override
	public Type visit(IntegerType n) {
		// TODO Auto-generated method stub
		return n;
	}

	@Override
	public Type visit(IdentifierType n) {
		// TODO Auto-generated method stub
		return tbl.lookup(Symbol.symbol(n.s));
	}

	@Override
	public Type visit(Block n) {
		// TODO Auto-generated method stub
		// Verifies for each statement in the block
		for(int i = 0; i < n.sl.size(); i++) {
			n.sl.elementAt(i).accept(this);
		}
		return null;
	}

	@Override
	public Type visit(If n) {
		// TODO Auto-generated method stub
		Type t = n.e.accept(this);
		if (t == null || !t.equals(new BooleanType())) {
			System.out.println("If conditional expression not evaluating to boolean.");
		}
		// Evaluates the statement in case the expression verifies
		n.s1.accept(this);
		// Evaluates the else statement
		n.s2.accept(this);
		
		return null;
	}

	@Override
	public Type visit(While n) {
		// TODO Auto-generated method stub
		Type t = n.e.accept(this);
		if (t == null || !t.equals(new BooleanType())) {
			System.out.println("While conditional expression not evaluating to boolean.");
		}
		// Evaluates the while statements
		n.s.accept(this);
		return null;
	}

	@Override
	public Type visit(Print n) {
		// TODO Auto-generated method stub
		Type t = n.e.accept(this);
		if (t == null || (!t.equals(new BooleanType()) && !t.equals(new IntegerType()))) {
			System.out.println("Print expression type invalid. Expected int or boolean.");
		}
		return null;
	}

	@Override
	public Type visit(Assign n) {
		// TODO Auto-generated method stub
		Type et = n.e.accept(this);
		Type at = n.i.accept(this);
		if (at == null || et == null || !et.equals(at)) {
			System.out.println("Assignment of incompatible types.");
		}
		return null;
	}

	@Override
	public Type visit(ArrayAssign n) {
		// TODO Auto-generated method stub
		Type at = n.i.accept(this);
		Type it = n.e1.accept(this);
		Type vt = n.e2.accept(this);
		
		if (it == null || !it.equals(new IntegerType())) {
			System.out.println("Invalid type for indexing expression.");
		}
		
		if (at == null || !at.equals(new IntArrayType())) {
			System.out.println("Expected Array type for array identifier.");
		}
		
		if (vt == null || !vt.equals(new IntegerType())) {
			System.out.println("Expected Integer type for assignment expression.");
		}
		return null;
	}

	@Override
	public Type visit(And n) {
		// TODO Auto-generated method stub
		Type t1 = n.e1.accept(this);
		Type t2 = n.e2.accept(this);
		if (t1 == null || t2 == null || !t1.equals(new BooleanType()) || !t2.equals(new BooleanType())) {
			System.out.println("Expected boolean type for expressions in an and expression.");
			return null;
		}
		return new BooleanType();
	}

	@Override
	public Type visit(LessThan n) {
		// TODO Auto-generated method stub
		Type t1 = n.e1.accept(this);
		Type t2 = n.e2.accept(this);
		if (t1 == null || t2 == null || !t1.equals(new IntegerType()) || !t2.equals(new IntegerType())) {
			System.out.println("Expected integer type for expressions in a less than expression.");
			return null;
		}
		return new BooleanType();
	}

	@Override
	public Type visit(Plus n) {
		// TODO Auto-generated method stub
		Type t1 = n.e1.accept(this);
		Type t2 = n.e2.accept(this);
		if (t1 == null || t2 == null || !t1.equals(new IntegerType()) || !t2.equals(new IntegerType())) {
			System.out.println("Expected integer type for expressions in a plus operation.");
			return null;
		}
		return new IntegerType();
	}

	@Override
	public Type visit(Minus n) {
		// TODO Auto-generated method stub
		Type t1 = n.e1.accept(this);
		Type t2 = n.e2.accept(this);
		if (t1 == null || t2 == null || !t1.equals(new IntegerType()) || !t2.equals(new IntegerType())) {
			System.out.println("Expected integer type for expressions in a minus operation.");
			return null;
		}
		return new IntegerType();
	}

	@Override
	public Type visit(Times n) {
		// TODO Auto-generated method stub
		Type t1 = n.e1.accept(this);
		Type t2 = n.e2.accept(this);
		if (t1 == null || t2 == null || !t1.equals(new IntegerType()) || !t2.equals(new IntegerType())) {
			System.out.println("Expected integer type for expressions in a times operation.");
			return null;
		}
		return new IntegerType();
	}

	@Override
	public Type visit(ArrayLookup n) {
		// TODO Auto-generated method stub
		return new IntegerType();
	}

	@Override
	public Type visit(ArrayLength n) {
		// TODO Auto-generated method stub
		return new IntegerType();
	}

	@Override
	public Type visit(Call n) {
		// TODO Auto-generated method stub
		Type t = n.e.accept(this);
		if (t == null || (!t.equals(new This()) && !t.equals(new IdentifierType(" ")) ) ) {
			System.out.println("Invalid object reference.");
		}
		
		Vector<symboltable.Method> methods = new Vector<symboltable.Method>();
		
		methods = tbl.get_class_methods(Symbol.symbol(((IdentifierType)t).s), Symbol.symbol(n.i.s));
		// Creating vector with call arguments types
		Vector<Formal> args = new Vector<Formal>();
		Type arg_type = null;
		for(int i = 0; i < n.el.size(); i++) {
			arg_type = n.el.elementAt(i).accept(this);
			args.add(new Formal(arg_type, new Identifier(" ")));
		}
		
		for(symboltable.Method m : methods) {
			if (m.compare_params_type(args)) {
				return m.get_return_type();
			}
		}
		System.out.println("Method " + n.i.s + " not defined in class " + ((IdentifierType)t).s  + ".");
		return null;
	}

	@Override
	public Type visit(IntegerLiteral n) {
		// TODO Auto-generated method stub
		return new IntegerType();
	}

	@Override
	public Type visit(True n) {
		// TODO Auto-generated method stub
		return new BooleanType();
	}

	@Override
	public Type visit(False n) {
		// TODO Auto-generated method stub
		return new BooleanType();
	}

	@Override
	public Type visit(IdentifierExp n) {
		// TODO Auto-generated method stub
		Type r = tbl.lookup(Symbol.symbol(n.s)); 
		if ( r == null ) {
			System.out.println("Identifier " + n.s + " was not declared in this scope.");
		}
		return r;
	}

	@Override
	public Type visit(This n) {
		// TODO Auto-generated method stub
		if (tbl.get_current_class() != null) {
			return tbl.get_current_class().get_type();
		}
		return null;
	}

	@Override
	public Type visit(NewArray n) {
		// TODO Auto-generated method stub
		Type t = n.e.accept(this);
		if (t == null || !t.equals(new IntegerType())) {
			System.out.println("Invalid type for new array expression. Expected int.");
			return null;
		}
		return new IntArrayType();
	}

	@Override
	public Type visit(NewObject n) {
		// TODO Auto-generated method stub
		Type t = n.i.accept(this);

		if (t == null || !tbl.contains_class(Symbol.symbol(((IdentifierType)t).s))) {
			System.out.println("Expected Class type.");
			return null;
		}
		return t;
	}

	@Override
	public Type visit(Not n) {
		// TODO Auto-generated method stub
		Type t = n.e.accept(this);
		if (t == null || !t.equals(new BooleanType())) {
			System.out.println("Expected boolean expression within not.");
			return null;
		}
		return new BooleanType();
	}

	@Override
	public Type visit(Identifier n) {
		// TODO Auto-generated method stub
		Type t = tbl.lookup(Symbol.symbol(n.s));
		if (t == null) {
			t = new IdentifierType(n.s);
		}
		return t;
	}

	@Override
	public Type visit(VoidType n) {
		// TODO Auto-generated method stub
		return n;
	}

}
